use std::ops::{Add, Sub};

use crate::msg::{Action, ExecuteMsg, InstantiateMsg, QueryMsg};
use crate::pool::{read_pool_by_pool_id, read_pools, store_pool, PoolInfo};
use crate::staker::{read_staker_by_address, store_staker, StakeInfo, StakePeriod, Staker};
use crate::state::{read_config, store_config, ConfigInfo, REWARDTOKENMAXDECIAML};
use cosmwasm_std::{log, Binary, HandleResponse, HandleResult, HumanAddr, StdError};
#[cfg(not(feature = "library"))]
use cosmwasm_std::{
    to_binary, Api, CosmosMsg, Env, Extern, InitResponse, Querier, StdResult, Storage, Uint128,
};
use hermit_toolkit_hmip21::asset::AssetInfo;
use hermit_toolkit_hmip21::{transfer_from_msg, transfer_msg};

const BOLCK_SIZE: usize = 256;

pub fn init<S: Storage, A: Api, Q: Querier>(
    deps: &mut Extern<S, A, Q>,
    env: Env,
    msg: InstantiateMsg,
) -> StdResult<InitResponse> {
    let owner = msg.owner.unwrap_or(env.message.sender);
    let config = ConfigInfo { owner };
    store_config(&mut deps.storage, &config)?;

    Ok(InitResponse {
        messages: vec![],
        log: vec![log("status", "success")], // See https://github.com/CosmWasm/wasmd/pull/386
    })
}

pub fn handle<S: Storage, A: Api, Q: Querier>(
    deps: &mut Extern<S, A, Q>,
    env: Env,
    msg: ExecuteMsg,
) -> HandleResult {
    match msg {
        ExecuteMsg::AddPool { pool } => execute_add_pool(deps, env, pool),
        ExecuteMsg::UpdatePool { pool } => execute_update_pool(deps, env, pool),
        ExecuteMsg::UpdateOwner { new_owner } => execute_update_owner(deps, env, new_owner),
        ExecuteMsg::Deposit {
            pool_id,
            stake_amount,
            stake_period,
        } => try_deposit(deps, env, pool_id, stake_amount, stake_period),
        ExecuteMsg::Withdraw {
            pool_id,
            withdraw_amount,
            withdraw_period,
        } => try_withdraw(deps, env, pool_id, withdraw_amount, withdraw_period),
        ExecuteMsg::WithdrawReward { pool_id } => try_withdraw_reward(deps, env, pool_id),
        ExecuteMsg::WithdrawStake { pool_id } => try_withdraw_stake(deps, env, pool_id),
    }
}

pub fn try_deposit<S: Storage, A: Api, Q: Querier>(
    deps: &mut Extern<S, A, Q>,
    env: Env,
    pool_id: u8,
    stake_amount: Uint128,
    stake_period: StakePeriod,
) -> HandleResult {
    // load pool info
    let mut is_first_stake_flexible = true;
    let current_blcok_time = env.block.time;
    let mut pool: PoolInfo = read_pool_by_pool_id(&deps.storage, pool_id)?;
    //  load staker info
    let mut staker = Staker {
        address: env.message.sender.clone(),
        pool_id: pool_id,
        pending_reward_amount: Uint128::zero(),
        pending_stake_amount: Uint128::zero(),
        stakeinfo: vec![],
    };
    let staker_m = read_staker_by_address(&deps.storage, pool_id, env.message.sender.clone());
    if staker_m.is_ok() {
        is_first_stake_flexible = false;
        staker = staker_m.unwrap();
        if staker.pool_id > 0u8 && !staker.stakeinfo.is_empty() {
            // update reward data
            update_reward(
                Action::Deposit("Deposit".to_string()),
                stake_period.clone(),
                stake_amount,
                current_blcok_time,
                &mut pool,
                &mut staker,
            );
        }
    }
    if pool.reward_amount < staker.pending_reward_amount {
        return Err(StdError::generic_err("not enough reward"));
    }
    if pool.stake_amount < staker.pending_stake_amount {
        return Err(StdError::generic_err("not enough stake"));
    }

    let mut transfer_msgs: Vec<CosmosMsg> = vec![];
    // Generate pending reward token transfer messages
    if staker.pending_reward_amount > Uint128::zero() {
        if let AssetInfo::Token {
            contract_addr,
            token_code_hash,
            ..
        } = &pool.reward_asset
        {
            transfer_msgs.push(transfer_msg(
                env.message.sender.clone(),
                staker.pending_reward_amount,
                None,
                None,
                BOLCK_SIZE,
                token_code_hash.into(),
                contract_addr.into(),
            )?);
        }
    }
    // Generate pending stake token transfer messages
    if staker.pending_stake_amount > Uint128::zero() {
        if let AssetInfo::Token {
            contract_addr,
            token_code_hash,
            ..
        } = &pool.stake_asset
        {
            transfer_msgs.push(transfer_msg(
                env.message.sender.clone(),
                staker.pending_stake_amount,
                None,
                None,
                BOLCK_SIZE,
                token_code_hash.into(),
                contract_addr.into(),
            )?);
        }
    }
    // Generate stake token transfer messages
    if let AssetInfo::Token {
        contract_addr,
        token_code_hash,
        ..
    } = &pool.stake_asset
    {
        transfer_msgs.push(transfer_from_msg(
            env.message.sender.clone(),
            env.contract.address.clone(),
            stake_amount,
            None,
            None,
            BOLCK_SIZE,
            token_code_hash.into(),
            contract_addr.into(),
        )?);
    };
    // add stakerinfo to staker
    let filter_pool = pool
        .stake_apy
        .iter()
        .find(|item| item.stake_period == stake_period);
    if !filter_pool.is_some() {
        return Err(StdError::generic_err("stakeperiod is not exists"));
    }
    let reward_per_block = filter_pool.unwrap().reward_per_block;
    let staker_asset_decimal = match pool.stake_asset {
        AssetInfo::Token {
            token_decimal,
            contract_addr: _,
            token_code_hash: _,
        } => token_decimal,
        AssetInfo::NativeToken { denom: _ } => 18,
    };
    match stake_period {
        StakePeriod::Flexible => {
            if is_first_stake_flexible {
                pool.flexible_stake_amount = pool.flexible_stake_amount.add(stake_amount);
                staker.stakeinfo.insert(
                    staker.stakeinfo.len(),
                    StakeInfo {
                        reward_amount: Uint128::zero(),
                        stake_amount,
                        stake_period,
                        start_lock_time: current_blcok_time,
                        end_lock_time: current_blcok_time,
                    },
                );
            }
        }
        StakePeriod::OneWeeks => {
            staker.stakeinfo.insert(
                staker.stakeinfo.len(),
                StakeInfo {
                    reward_amount: stake_amount.multiply_ratio(
                        reward_per_block.multiply_ratio(86400u64 * 7u64, 1u64),
                        10u32.pow(staker_asset_decimal),
                    ),
                    stake_amount,
                    stake_period,
                    start_lock_time: current_blcok_time,
                    end_lock_time: current_blcok_time + 86400 * 7,
                },
            );
        }
        StakePeriod::FiveWeeks => {
            staker.stakeinfo.insert(
                staker.stakeinfo.len(),
                StakeInfo {
                    reward_amount: stake_amount.multiply_ratio(
                        reward_per_block.multiply_ratio(86400u64 * 7u64 * 5u64, 1u64),
                        10u32.pow(staker_asset_decimal),
                    ),
                    stake_amount,
                    stake_period,
                    start_lock_time: current_blcok_time,
                    end_lock_time: current_blcok_time + 86400 * 7 * 5,
                },
            );
        }
        StakePeriod::TenWeeks => {
            staker.stakeinfo.insert(
                staker.stakeinfo.len(),
                StakeInfo {
                    reward_amount: stake_amount.multiply_ratio(
                        reward_per_block.multiply_ratio(86400u64 * 7u64 * 10u64, 1u64),
                        10u32.pow(staker_asset_decimal),
                    ),
                    stake_amount,
                    start_lock_time: current_blcok_time,
                    stake_period,
                    end_lock_time: current_blcok_time + 86400 * 7 * 10,
                },
            );
        }
        StakePeriod::TwentyFiveWeeks => {
            staker.stakeinfo.insert(
                staker.stakeinfo.len(),
                StakeInfo {
                    reward_amount: stake_amount.multiply_ratio(
                        reward_per_block.multiply_ratio(86400u64 * 7u64 * 52u64, 1u64),
                        10u32.pow(staker_asset_decimal),
                    ),
                    stake_amount,
                    start_lock_time: current_blcok_time,
                    stake_period,
                    end_lock_time: current_blcok_time + 86400 * 7 * 52,
                },
            );
        }
    };
    // Update pools info
    pool.reward_amount = pool.reward_amount.sub(staker.pending_reward_amount)?;
    pool.stake_amount = pool
        .stake_amount
        .add(stake_amount)
        .sub(staker.pending_stake_amount)?;
    store_pool(&mut deps.storage, &pool)?;
    // Update stakers info
    staker.pending_reward_amount = Uint128::zero();
    staker.pending_stake_amount = Uint128::zero();
    store_staker(&mut deps.storage, &staker)?;

    Ok(HandleResponse {
        messages: transfer_msgs,
        log: vec![
            log("action", "deposit"),
            log("pending_reward_amount", staker.pending_reward_amount),
            log("pending_stake_amount", staker.pending_stake_amount),
            log("stake_amount", stake_amount),
        ],
        data: None,
    })
}

pub fn try_withdraw<S: Storage, A: Api, Q: Querier>(
    deps: &mut Extern<S, A, Q>,
    env: Env,
    pool_id: u8,
    withdraw_amount: Uint128,
    withdraw_period: StakePeriod,
) -> HandleResult {
    // load pool info
    let mut pool = read_pool_by_pool_id(&deps.storage, pool_id)?;
    // load staker info
    let mut staker = read_staker_by_address(&deps.storage, pool_id, env.message.sender.clone())?;
    if staker.pool_id > 0u8 && !staker.stakeinfo.is_empty() {
        // update reward data
        update_reward(
            Action::WithDraw("WithDraw".to_string()),
            withdraw_period.clone(),
            withdraw_amount,
            env.block.time,
            &mut pool,
            &mut staker,
        );
    }
    if pool.reward_amount < staker.pending_reward_amount {
        return Err(StdError::generic_err("not enough reward"));
    }
    // if pool.stake_amount < staker.pending_stake_amount {
    //     return Err(StdError::generic_err("not enough stake"));
    // }
    // if staker.pending_reward_amount == Uint128::zero() {
    //     return Err(StdError::generic_err("no reward"));
    // }

    let mut transfer_msgs: Vec<CosmosMsg> = vec![];
    // Generate pending reward token transfer messages
    if staker.pending_reward_amount > Uint128::zero() {
        if let AssetInfo::Token {
            contract_addr,
            token_code_hash,
            ..
        } = &pool.reward_asset
        {
            transfer_msgs.push(transfer_msg(
                env.message.sender.clone(),
                staker.pending_reward_amount,
                None,
                None,
                BOLCK_SIZE,
                token_code_hash.into(),
                contract_addr.into(),
            )?);
        }
    }
    // Generate pending stake token transfer messages
    if staker.pending_stake_amount > Uint128::zero() {
        if let AssetInfo::Token {
            contract_addr,
            token_code_hash,
            ..
        } = &pool.stake_asset
        {
            transfer_msgs.push(transfer_msg(
                env.message.sender.clone(),
                staker.pending_stake_amount,
                None,
                None,
                BOLCK_SIZE,
                token_code_hash.into(),
                contract_addr.into(),
            )?);
        }
    }
    // Update pools info
    pool.stake_amount = pool.stake_amount.sub(staker.pending_stake_amount)?;
    pool.reward_amount = pool.reward_amount.sub(staker.pending_reward_amount)?;
    store_pool(&mut deps.storage, &pool)?;
    // Update stakers info
    staker.pending_reward_amount = Uint128::zero();
    staker.pending_stake_amount = Uint128::zero();
    store_staker(&mut deps.storage, &staker)?;

    Ok(HandleResponse {
        messages: transfer_msgs,
        log: vec![
            log("action", "withdraw"),
            log("pool_id", pool.pool_id),
            log("pending_reward_amount", staker.pending_reward_amount),
            log("pending_stake_amount", staker.pending_stake_amount),
        ],
        data: None,
    })
}

pub fn update_reward(
    action_type: Action,
    operate_period: StakePeriod,
    operate_amount: Uint128,
    current_block_time: u64,
    pool: &mut PoolInfo,
    staker: &mut Staker,
) {
    let mut pending_reward_amount = Uint128::zero();
    let mut pending_stake_amount = Uint128::zero();
    let mut flexible_stake = StakeInfo {
        reward_amount: Uint128::zero(),
        stake_amount: operate_amount,
        stake_period: StakePeriod::Flexible,
        start_lock_time: current_block_time,
        end_lock_time: current_block_time,
    };
    let flexible_pool = pool
        .stake_apy
        .iter()
        .find(|item| item.stake_period == StakePeriod::Flexible);
    if !flexible_pool.is_some() {
        print!("flexible_pool is not exists");
    }
    let staker_asset_decimal = match pool.stake_asset {
        AssetInfo::Token {
            token_decimal,
            contract_addr: _,
            token_code_hash: _,
        } => token_decimal,
        AssetInfo::NativeToken { denom: _ } => 0,
    };
    staker.stakeinfo.retain(|item| {
        if item.stake_period == StakePeriod::Flexible && operate_period == StakePeriod::Flexible {
            if flexible_pool.is_some() {
                flexible_stake.stake_amount = item.stake_amount;

                pending_reward_amount = flexible_pool
                    .unwrap()
                    .reward_per_block
                    .multiply_ratio(item.stake_amount, 1u64)
                    .multiply_ratio(
                        current_block_time - item.start_lock_time,
                        10u32.pow(staker_asset_decimal),
                    );

                match action_type.clone() {
                    Action::Deposit(d) => {
                        if d == "Deposit" {
                            pool.flexible_stake_amount =
                                pool.flexible_stake_amount.add(operate_amount);
                            pending_stake_amount = Uint128::zero();
                            flexible_stake.stake_amount += operate_amount;
                        }
                    }
                    Action::WithDraw(w) => {
                        if w == "WithDraw" {
                            if operate_amount > item.stake_amount {
                                print!(
                                    "operate_amount {} item.stake_amount {} operate_amount out of balance",
                                    operate_amount, item.stake_amount
                                );
                            }
                            //Only applies within withdraw_lock_period of staking. Unstaking after withdraw_lock_period will not include a fee.
                            if current_block_time < item.start_lock_time + pool.withdraw_lock_period
                            {
                                let tm_pending_stake_amount = operate_amount.multiply_ratio(
                                    REWARDTOKENMAXDECIAML - pool.withdraw_fee,
                                    REWARDTOKENMAXDECIAML,
                                );
                                pool.flexible_stake_amount = pool
                                    .flexible_stake_amount
                                    .sub(tm_pending_stake_amount)
                                    .unwrap();
                                pending_stake_amount += tm_pending_stake_amount;
                                flexible_stake.stake_amount = flexible_stake
                                    .stake_amount
                                    .sub(tm_pending_stake_amount)
                                    .unwrap();
                            } else {
                                pool.flexible_stake_amount =
                                    pool.flexible_stake_amount.sub(operate_amount).unwrap();
                                pending_stake_amount += operate_amount;
                                flexible_stake.stake_amount =
                                    flexible_stake.stake_amount.sub(operate_amount).unwrap();
                            }
                        }
                    }
                }
            }
            false
        } else {
            if operate_period == item.stake_period && current_block_time >= item.end_lock_time {
                pending_reward_amount += item.reward_amount;
                pending_stake_amount += item.stake_amount;
                false
            } else {
                true
            }
        }
    });

    staker.pending_reward_amount = pending_reward_amount;
    staker.pending_stake_amount = pending_stake_amount;
    match operate_period {
        StakePeriod::Flexible => {
            match action_type.clone() {
                Action::Deposit(d) => {
                    if d == "Deposit"
                        && !staker
                            .stakeinfo
                            .iter()
                            .find(|item| item.stake_period == StakePeriod::Flexible)
                            .is_some()
                    {
                        pool.flexible_stake_amount = pool.flexible_stake_amount.add(operate_amount);
                    }
                }
                _ => (),
            }
            if flexible_stake.stake_amount > Uint128::zero() {
                staker
                    .stakeinfo
                    .insert(staker.stakeinfo.len(), flexible_stake);
            }
        }
        _ => (),
    }
}

pub fn try_withdraw_reward<S: Storage, A: Api, Q: Querier>(
    deps: &mut Extern<S, A, Q>,
    env: Env,
    pool_id: u8,
) -> StdResult<HandleResponse> {
    let config = read_config(&deps.storage)?;
    // authorize owner
    if env.message.sender != config.owner {
        return Err(StdError::unauthorized());
    }
    // load pool info
    let mut pool = read_pool_by_pool_id(&deps.storage, pool_id)?;
    let mut transfer_msgs: Vec<CosmosMsg> = vec![];
    // create transfer msg
    if let AssetInfo::Token {
        contract_addr,
        token_code_hash,
        ..
    } = &pool.reward_asset
    {
        transfer_msgs.push(transfer_msg(
            env.message.sender.clone(),
            pool.reward_amount,
            None,
            None,
            BOLCK_SIZE,
            token_code_hash.clone(),
            contract_addr.clone(),
        )?);
    }

    pool.reward_amount = Uint128::zero();
    store_pool(&mut deps.storage, &pool)?;

    Ok(HandleResponse {
        messages: transfer_msgs,
        log: vec![
            log("action", "withdraw_reward"),
            log("pool_id", pool.pool_id),
            log("reward_amount", pool.reward_amount),
        ],
        data: None,
    })
}

pub fn try_withdraw_stake<S: Storage, A: Api, Q: Querier>(
    deps: &mut Extern<S, A, Q>,
    env: Env,
    pool_id: u8,
) -> StdResult<HandleResponse> {
    let config = read_config(&deps.storage)?;
    // authorize owner
    if env.message.sender != config.owner {
        return Err(StdError::unauthorized());
    }
    // load pool info
    let mut pool: PoolInfo = read_pool_by_pool_id(&deps.storage, pool_id)?;
    let mut transfer_msgs: Vec<CosmosMsg> = vec![];
    // create transfer msg
    if let AssetInfo::Token {
        contract_addr,
        token_code_hash,
        ..
    } = &pool.stake_asset
    {
        transfer_msgs.push(transfer_msg(
            env.message.sender.clone(),
            pool.stake_amount,
            None,
            None,
            BOLCK_SIZE,
            token_code_hash.clone(),
            contract_addr.clone(),
        )?);
    }

    pool.stake_amount = Uint128::zero();
    store_pool(&mut deps.storage, &pool)?;

    Ok(HandleResponse {
        messages: transfer_msgs,
        log: vec![
            log("action", "withdraw_stake"),
            log("pool_id", pool.pool_id),
            log("stake_amount", pool.stake_amount),
        ],
        data: None,
    })
}

pub fn execute_add_pool<S: Storage, A: Api, Q: Querier>(
    deps: &mut Extern<S, A, Q>,
    env: Env,
    pool: PoolInfo,
) -> HandleResult {
    let config = read_config(&deps.storage)?;
    // authorize owner
    if env.message.sender != config.owner {
        return Err(StdError::unauthorized());
    }
    if read_pool_by_pool_id(&deps.storage, pool.pool_id).is_ok() {
        return Err(StdError::generic_err("pool has exists"));
    };

    store_pool(&mut deps.storage, &pool)?;

    Ok(HandleResponse {
        messages: vec![],
        log: vec![log("action", "add_pool")],
        data: None,
    })
}

pub fn execute_update_pool<S: Storage, A: Api, Q: Querier>(
    deps: &mut Extern<S, A, Q>,
    env: Env,
    pool: PoolInfo,
) -> HandleResult {
    let config = read_config(&deps.storage)?;
    // authorize owner
    if env.message.sender != config.owner {
        return Err(StdError::unauthorized());
    }
    if read_pool_by_pool_id(&deps.storage, pool.pool_id).is_err() {
        return Err(StdError::generic_err("pool no exists"));
    };

    store_pool(&mut deps.storage, &pool)?;

    Ok(HandleResponse {
        messages: vec![],
        log: vec![log("action", "update_pool"), log("pool_id", pool.pool_id)],
        data: None,
    })
}

pub fn execute_update_owner<S: Storage, A: Api, Q: Querier>(
    deps: &mut Extern<S, A, Q>,
    env: Env,
    new_owner: HumanAddr,
) -> HandleResult {
    let mut config = read_config(&deps.storage)?;
    // authorize owner
    if env.message.sender != config.owner {
        return Err(StdError::unauthorized());
    }
    config.owner = new_owner;
    store_config(&mut deps.storage, &config)?;

    Ok(HandleResponse {
        messages: vec![],
        log: vec![log("status", "success")], // See https://github.com/CosmWasm/wasmd/pull/386
        data: None,
    })
}

pub fn query<S: Storage, A: Api, Q: Querier>(
    deps: &Extern<S, A, Q>,
    msg: QueryMsg,
) -> StdResult<Binary> {
    match msg {
        QueryMsg::Config() => to_binary(&query_config(&deps)?),
        QueryMsg::Pool { pool_id } => to_binary(&query_pool(&deps, pool_id)?),
        QueryMsg::Pools {} => to_binary(&query_pools(deps)?),
        QueryMsg::Staker {
            address,
            block_time,
            pool_id,
        } => to_binary(&query_staker(deps, address, block_time, pool_id)?),
    }
}

pub fn query_config<S: Storage, A: Api, Q: Querier>(
    deps: &Extern<S, A, Q>,
) -> StdResult<ConfigInfo> {
    Ok(read_config(&deps.storage)?)
}

fn query_pool<S: Storage, A: Api, Q: Querier>(
    deps: &Extern<S, A, Q>,
    pool_id: u8,
) -> StdResult<PoolInfo> {
    Ok(read_pool_by_pool_id(&deps.storage, pool_id)?)
}

fn query_pools<S: Storage, A: Api, Q: Querier>(deps: &Extern<S, A, Q>) -> StdResult<Vec<PoolInfo>> {
    Ok(read_pools(&deps)?)
}

fn query_staker<S: Storage, A: Api, Q: Querier>(
    deps: &Extern<S, A, Q>,
    address: HumanAddr,
    block_time: u64,
    pool_id: u8,
) -> StdResult<Staker> {
    let pool = read_pool_by_pool_id(&deps.storage, pool_id)?;
    let staker = read_staker_by_address(&deps.storage, pool_id, address)?;
    let mut pending_reward_amount = Uint128::zero();
    let mut pending_stake_amount = Uint128::zero();
    let flexible_pool = pool
        .stake_apy
        .iter()
        .find(|item| item.stake_period == StakePeriod::Flexible);
    let staker_asset_decimal = match pool.stake_asset {
        AssetInfo::Token {
            token_decimal,
            contract_addr: _,
            token_code_hash: _,
        } => token_decimal,
        AssetInfo::NativeToken { denom: _ } => 0,
    };
    let mut tm_stakeinfos: Vec<StakeInfo> = vec![];
    for (_, item) in staker.stakeinfo.iter().enumerate() {
        if item.stake_period == StakePeriod::Flexible {
            if flexible_pool.is_some() {
                // tm_stakeinfos.insert(tm_stakeinfo.len()-1, item.clone());
                let tm_reward_amount = flexible_pool
                    .unwrap()
                    .reward_per_block
                    .multiply_ratio(item.stake_amount, 1u64)
                    .multiply_ratio(
                        block_time - item.start_lock_time,
                        10u32.pow(staker_asset_decimal),
                    );
                pending_reward_amount += tm_reward_amount;
                //Only applies within withdraw_lock_period of staking. Unstaking after withdraw_lock_period will not include a fee.
                if block_time < item.start_lock_time + pool.withdraw_lock_period {
                    pending_stake_amount += pending_stake_amount.multiply_ratio(
                        REWARDTOKENMAXDECIAML - pool.withdraw_fee,
                        REWARDTOKENMAXDECIAML,
                    )
                } else {
                    pending_stake_amount += item.stake_amount;
                }
                tm_stakeinfos.push(StakeInfo {
                    reward_amount: tm_reward_amount,
                    ..item.clone()
                });
            }
        } else {
            if block_time >= item.end_lock_time {
                pending_reward_amount += item.reward_amount;
                pending_stake_amount += item.stake_amount;
            }
            tm_stakeinfos.push(item.clone());
        }
    }

    Ok(Staker {
        pending_reward_amount: pending_reward_amount,
        pending_stake_amount: pending_stake_amount,
        stakeinfo: tm_stakeinfos,
        ..staker
    })
}
